/*
 * session.c
 * This file is part of gURL
 *
 * Copyright (C) 2013, 2014 - Aleksey Konovalov
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Library General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Library General Public License for more details.
 *
 *  You should have received a copy of the GNU Library General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "session.h"

#include <assert.h>
#include "defines.h"
#include <glib/gi18n.h>
#include <stdbool.h>
#include <string.h>
#include "utils.h"

static gboolean timer_func(SessionClass* sessioncl);
static void session_dispose(GObject* object);

G_DEFINE_TYPE(Session, session, G_TYPE_OBJECT)

/*
 * class methods
 */

static void session_class_init(SessionClass* klass) {
    G_OBJECT_CLASS(klass)->dispose = session_dispose;

    g_signal_new("update", SESSION_TYPE, G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
                 NULL, G_TYPE_NONE, 1, G_TYPE_INT);

    klass->curlm = curl_multi_init();
    if(!klass->curlm)
        g_error(_("curl initialization failed"));
    g_timeout_add(100, (GSourceFunc)&timer_func, klass);
}

//static void session_class_finalize(gpointer g_class) {
//puts("class finalize");
//    curl_multi_cleanup(SESSION_CLASS(g_class)->curlm);
//}

/*
 * instance methods
 */

static void session_init(Session* self) {
    self->request.url = self->request.body = NULL;
    self->request.headers = g_string_sized_new(1024);
    self->request.body_encoding = UNKNOWN_ENCODING;
    self->response.status = 0;
    self->response.headers = g_string_sized_new(1024);
    self->response.body = g_string_sized_new(1024);
    self->response.content_type = g_string_sized_new(32);
    self->response.content_encoding = UNKNOWN_ENCODING;
    self->cURLheaders = NULL;
}

static void session_dispose(GObject* object) {
    Session* session = SESSION(object);
    g_string_free(session->request.url, TRUE);
    g_string_free(session->request.headers, TRUE);
    g_string_free(session->request.body, TRUE);
    g_string_free(session->response.body, TRUE);
    g_string_free(session->response.content_type, TRUE);
    g_string_free(session->response.headers, TRUE);
}

static void set_headers(CURL* curl, struct curl_slist** cURLheaders, const char* headers) {
    const char* token = strtok((char*)headers, "\n");
    gboolean expect100_found = FALSE;
    while(token) {
        *cURLheaders = curl_slist_append(*cURLheaders, token);
        if(!expect100_found)
            expect100_found = g_ascii_strncasecmp(token, "Expect:", 7) == 0;
        token = strtok(NULL, "\n");
    }
    if(!expect100_found) // disable automatic addition of the expect header
        *cURLheaders = curl_slist_append(*cURLheaders, "Expect:");
    if(*cURLheaders)
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *cURLheaders);
}

static size_t receive_headers_cb(void *ptr, size_t size, size_t nmemb,
                                 void *userdata) {
    Session* session = userdata;
    g_string_append_len(session->response.headers, ptr, size * nmemb);
    g_signal_emit_by_name(G_OBJECT(session), "update", uRESPONSE_HEADERS);
    return size * nmemb;
}

static size_t receive_body_cb( void *ptr, size_t size, size_t nmemb,
                               void *userdata ) {
    Session* session = userdata;
    g_string_append_len(session->response.body, ptr, size * nmemb);
    g_signal_emit_by_name(G_OBJECT(session), "update", uRESPONSE_BODY);
    return size * nmemb;
}

static size_t send_body_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
    Session* session = (Session*)userdata;
    size_t cnt = MIN(nmemb * size,
                     session->request.body->len - session->body_sended_bytes);
    memcpy(ptr, session->request.body->str + session->body_sended_bytes, cnt);
    session->body_sended_bytes += cnt;
    return cnt;
}

static int debug_cb(CURL* UNUSED(handle), curl_infotype type, char* data,
                    size_t size, void* userptr) {
    Session* session = userptr;
    assert(session);
    if(type == CURLINFO_HEADER_OUT) {
        g_string_append_len(session->request.headers, data, size);
        g_signal_emit_by_name(G_OBJECT(session), "update", uREQUEST_HEADERS);
        puts("<<header>>");
    }
    else if(type == CURLINFO_HEADER_IN)
        puts(">>HEADER<<");
    else if(type == CURLINFO_DATA_IN)
        puts(">>DATA<<");
    else if(type == CURLINFO_DATA_OUT)
        puts("<<data>>");
    else if(type == CURLINFO_TEXT)
        puts("<<text>>");
    else
        return 0;
//    printf("|%s|\n", data);
    return 0;
}

static gboolean timer_func(SessionClass* sessioncl) {
   curl_multi_perform(sessioncl->curlm, &sessioncl->curl_handle_count);
   CURLMsg* msg;
   while((msg = curl_multi_info_read(sessioncl->curlm,
                                     &sessioncl->curl_handle_count)))
      if(msg->msg == CURLMSG_DONE) {
         printf("session ok\n");
         CURL* easy = msg->easy_handle;
         Session* session = NULL;
         curl_easy_getinfo(easy, CURLINFO_PRIVATE, (void*)&session);
         if(session) {
            if(session->cURLheaders) {
                curl_slist_free_all(session->cURLheaders);
                session->cURLheaders = NULL;
            }
            curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE,
                              &session->response.status);
            curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME,
                              &session->statistics.total_time);
            curl_easy_getinfo(easy, CURLINFO_NAMELOOKUP_TIME,
                              &session->statistics.namelookup_time);
            curl_easy_getinfo(easy, CURLINFO_CONNECT_TIME,
                              &session->statistics.connect_time);
            curl_easy_getinfo(easy, CURLINFO_PRETRANSFER_TIME,
                              &session->statistics.pretransfer_time);
            char* content_type = NULL;
            curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &content_type);
            g_string_assign(session->response.content_type, content_type ? content_type : "");
            session->response.content_encoding =
                determine_encoding_by_content_type(session->response.content_type);
            session->statistics.str_error = curl_easy_strerror(msg->data.result);
            g_signal_emit_by_name(G_OBJECT(session), "update", uRESPONSE_STATUS);
            printf("session finished: %ld\n", session->response.status);
            g_object_unref(session);
         }
         curl_multi_remove_handle(sessioncl->curlm, easy);
         curl_easy_cleanup(easy);
      }
   return true;
}

static void configure_curl_method(CURL* curl, method_type mtype, Session* session) {
    switch(mtype) {
        case mPOST:
            curl_easy_setopt(curl, CURLOPT_POST, 1);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, session->request.body->len);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, session->request.body->str);
            break;
        case mGET: curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); break;
        case mHEAD: curl_easy_setopt(curl, CURLOPT_NOBODY, 1); break;
        case mDELETE: curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); break;
        case mPUT:
            curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
            curl_easy_setopt(curl, CURLOPT_READDATA, session);
            curl_easy_setopt(curl, CURLOPT_READFUNCTION, &send_body_cb);
            curl_easy_setopt(curl, CURLOPT_INFILESIZE, session->request.body->len);
            break;
        default: assert(0);
    }
}

static void request_exec(Session* result, method_type mtype, const char* url,
                         const char* headers, const char* body, gsize body_len) {
    CURL* curl = curl_easy_init();
    if(!curl)
        g_error(_("failed to initialize curl"));

    SessionClass* sessioncl = SESSION_GET_CLASS(result);

    result->body_sended_bytes = 0;
    result->request.url = g_string_new(url);
    result->request.body = g_string_new_len(body, body_len);

    g_object_ref(result);
    curl_easy_setopt(curl, CURLOPT_PRIVATE, result);

    curl_easy_setopt(curl, CURLOPT_URL, result->request.url->str);
    configure_curl_method(curl, mtype, result);
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, receive_headers_cb);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, result);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive_body_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, result);
    curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    set_headers(curl, &result->cURLheaders, headers);

    curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_cb);
    curl_easy_setopt(curl, CURLOPT_DEBUGDATA, result);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);

    curl_multi_add_handle(sessioncl->curlm, curl);
}

const GString* session_get_request_url(Session* session) {
    assert(session);
    return session->request.url;
}

long session_get_http_status(Session* session) {
    assert(session);
    return session->response.status;
}

const GString* session_get_response_headers(Session* session) {
    assert(session);
    return session->response.headers;
}

const GString* session_get_response_body(Session* session) {
    assert(session);
    return session->response.body;
}

const GString* session_get_request_headers(Session* session) {
    assert(session);
    return session->request.headers;
}

const GString* session_get_request_body(Session* session) {
    assert(session);
    return session->request.body;
}

Encoding session_get_request_body_encoding(Session* session) {
    assert(session);
    return session->request.body_encoding;
}

const GString* session_get_response_content_type(Session* session) {
    assert(session);
    return session->response.content_type;
}

Encoding session_get_response_content_encoding(Session* session) {
    assert(session);
    return session->response.content_encoding;
}

void session_set_response_content_encoding(Session* session, Encoding enc) {
    assert(session);
    session->response.content_encoding = enc;
}

Session* session_new(method_type mtype, const char* url,
                     const char* headers, const char* body, gsize body_len,
                     Encoding body_encoding) {
    Session* session = g_object_new(SESSION_TYPE, NULL);
    session->request.body_encoding = body_encoding;
    request_exec(session, mtype, url, headers, body, body_len);
    return session;
}
